{% extends "console/layout.html" %}
{% block title %}Call Records · {{site_name|default('RustPBX')}}{% endblock %}
{% block content %}
<div class="p-6">
    <div class="mx-auto max-w-7xl space-y-6" x-data='callRecordsConsole({
            basePath: {{ (base_path | default("/console")) | tojson }},
            filter_options: {{ filter_options | default({}) | tojson }},
            list_url: {{ list_url | default("/console/call-records") | tojson }},
            page_size_options: {{ page_size_options | default([10, 25, 50]) | tojson }}
        })' x-init="init()">
        <header class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
            <div class="space-y-1">
                <p class="text-xs font-semibold uppercase tracking-wide text-sky-600">Voice analytics</p>
                <h1 class="text-2xl font-semibold text-slate-900">Call records</h1>
            </div>
            <div class="flex flex-wrap items-center gap-3">
                <button type="button"
                    class="inline-flex items-center gap-2 rounded-lg bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2"
                    @click="exportCsv">
                    <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M4 12l6 6 6-6" />
                        <path stroke-linecap="round" stroke-linejoin="round" d="M10 18V4" />
                    </svg>
                    Export CSV
                </button>
            </div>
        </header>

        <section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
            <div class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5 sm:col-span-2 xl:col-span-1">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Total calls</div>
                <div class="mt-2 flex items-baseline gap-2">
                    <span class="text-2xl font-semibold text-slate-900" x-text="summary.total"></span>
                    <span class="text-xs text-slate-500">records</span>
                </div>
                <div class="mt-3 space-y-1 text-xs text-slate-600 xl:hidden">
                    <div class="flex items-center justify-between">
                        <span>Total minutes</span>
                        <span class="font-semibold text-slate-900" x-text="formatNumber(summary.total_minutes)"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Billable minutes</span>
                        <span class="font-semibold text-slate-900"
                            x-text="formatNumber(summary.billing_billable_minutes)"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Transcribed</span>
                        <span class="font-semibold text-sky-600" x-text="summary.transcribed"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Total billed</span>
                        <span class="font-semibold text-slate-900" x-text="billingSummaryTotal()"></span>
                    </div>
                </div>
            </div>
            <div class="hidden rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5 xl:block">
                <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">Completion</div>
                <div class="hidden flex-col gap-1 text-[11px] text-slate-600 sm:flex">
                    <div class="flex items-center justify-between">
                        <span>Answered</span>
                        <span class="font-semibold text-emerald-600" x-text="summary.answered"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Missed</span>
                        <span class="font-semibold text-amber-600" x-text="summary.missed"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Failed</span>
                        <span class="font-semibold text-rose-600" x-text="summary.failed"></span>
                    </div>
                </div>
                <div class="mt-1 flex flex-wrap gap-2 text-xs text-slate-600 sm:hidden">
                    <span>Ans <span class="font-semibold text-emerald-600" x-text="summary.answered"></span></span>
                    <span>Miss <span class="font-semibold text-amber-600" x-text="summary.missed"></span></span>
                    <span>Fail <span class="font-semibold text-rose-600" x-text="summary.failed"></span></span>
                </div>
            </div>
            <div class="hidden rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5 xl:block">
                <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">Utilisation</div>
                <div class="hidden flex-col gap-1 text-[11px] text-slate-600 sm:flex">
                    <div class="flex items-center justify-between">
                        <span>Avg duration</span>
                        <span class="font-semibold text-slate-900"
                            x-text="formatAvgDuration(summary.avg_duration)"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Total minutes</span>
                        <span class="font-semibold text-slate-900" x-text="formatNumber(summary.total_minutes)"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Unique DIDs</span>
                        <span class="font-semibold text-slate-900" x-text="summary.unique_dids"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Billable minutes</span>
                        <span class="font-semibold text-slate-900"
                            x-text="formatNumber(summary.billing_billable_minutes)"></span>
                    </div>
                </div>
                <div class="mt-1 flex flex-wrap gap-2 text-xs text-slate-600 sm:hidden">
                    <span>Avg <span class="font-semibold text-slate-900"
                            x-text="formatAvgDuration(summary.avg_duration)"></span></span>
                    <span>Min <span class="font-semibold text-slate-900"
                            x-text="formatNumber(summary.total_minutes)"></span></span>
                    <span>Bill <span class="font-semibold text-slate-900"
                            x-text="formatNumber(summary.billing_billable_minutes)"></span></span>
                </div>
            </div>
            <div class="hidden rounded-xl bg-white p-3 shadow-sm ring-1 ring-black/5 xl:block">
                <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">ASR coverage</div>
                <div class="hidden items-baseline gap-2 sm:flex">
                    <span class="text-xl font-semibold text-sky-600" x-text="summary.transcribed"></span>
                    <span class="text-[11px] text-slate-500">transcribed</span>
                </div>
                <div class="mt-1 text-xs text-slate-600 sm:hidden">
                    <span class="font-semibold text-sky-600" x-text="summary.transcribed"></span>
                    <span>transcribed</span>
                </div>
                <p class="mt-2 hidden text-[11px] text-slate-400 sm:block" x-show="summary.transcribed">Ready for
                    semantic search</p>
            </div>
            <div class="hidden rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5 xl:block">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Billing</div>
                <div class="mt-2 space-y-1 text-xs text-slate-600">
                    <div class="flex items-center justify-between">
                        <span>Charged calls</span>
                        <span class="font-semibold text-slate-900"
                            x-text="formatNumber(summary.billing_charged_calls)"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Rated calls</span>
                        <span class="font-semibold text-slate-900"
                            x-text="formatNumber(summary.billing_rated_calls)"></span>
                    </div>
                    <div class="flex items-center justify-between">
                        <span>Total billed</span>
                        <span class="font-semibold text-slate-900" x-text="billingSummaryTotal()"></span>
                    </div>
                </div>
            </div>
        </section>

        <section class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
            <div class="flex flex-col gap-4">
                <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
                    <div class="flex flex-wrap items-center gap-4">
                        <div class="flex items-center gap-2">
                            <div class="inline-flex overflow-hidden rounded-full border border-slate-200 bg-white">
                                <template x-for="(tab, idx) in statusTabs()" :key="`status-${tab.value}`">
                                    <button type="button"
                                        class="px-3 py-1.5 text-xs font-semibold transition focus:outline-none" :class="[
                                            statusFilter === tab.value ? 'bg-sky-600 text-white' : 'text-slate-600 hover:bg-slate-100',
                                            idx < statusTabs().length - 1 ? 'border-r border-slate-200' : ''
                                        ]" @click="selectStatusTab(tab.value)">
                                        <span x-text="tab.label"></span>
                                    </button>
                                </template>
                            </div>
                        </div>
                        <div class="flex items-center gap-2">
                            <div class="inline-flex overflow-hidden rounded-full border border-slate-200 bg-white">
                                <template x-for="(tab, idx) in directionTabs()" :key="`direction-${tab.value}`">
                                    <button type="button"
                                        class="px-3 py-1.5 text-xs font-semibold transition focus:outline-none" :class="[
                                            directionFilter === tab.value ? 'bg-gray-600 text-white' : 'text-slate-600 hover:bg-slate-100',
                                            idx < directionTabs().length - 1 ? 'border-r border-slate-200' : ''
                                        ]" @click="selectDirectionTab(tab.value)">
                                        <span x-text="tab.label"></span>
                                    </button>
                                </template>
                            </div>
                        </div>
                    </div>
                    <div
                        class="flex w-full flex-col gap-3 lg:w-auto lg:flex-row lg:items-center lg:justify-end lg:gap-3">
                        <label class="relative flex-1 lg:min-w-[18rem]" aria-label="Search call records">
                            <svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400"
                                viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6">
                                <path stroke-linecap="round" stroke-linejoin="round"
                                    d="m17.5 17.5-3.65-3.65m0 0a5.5 5.5 0 1 0-7.778-7.778 5.5 5.5 0 0 0 7.778 7.778Z" />
                            </svg>
                            <input type="search" x-model.trim="search" @input.debounce.400ms="scheduleFetch()"
                                class="w-full rounded-lg border border-slate-200 px-3 py-2 pl-9 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
                                placeholder="Search number, agent, tag...">
                        </label>
                        <div class="flex flex-wrap items-center gap-2">
                            <select aria-label="Add filter"
                                class="rounded-lg border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-600 transition hover:border-sky-300 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
                                @change="addFilter($event)">
                                <option value="">+ Add filter</option>
                                <template x-for="option in filterSelectOptions" :key="option.key">
                                    <option :value="option.key" :disabled="activeFilters.includes(option.key)"
                                        x-text="option.label"></option>
                                </template>
                            </select>
                            <select aria-label="Sort call records" x-model="sortOrder" @change="applySorting()"
                                class="rounded-lg border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-600 transition hover:border-sky-300 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200">
                                <template x-for="option in sortOptions" :key="option.value">
                                    <option :value="option.value" x-text="option.label"></option>
                                </template>
                            </select>
                            <button type="button"
                                class="inline-flex items-center gap-2 rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700"
                                @click="clearFilters()">
                                <svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                    stroke-width="1.6">
                                    <path stroke-linecap="round" stroke-linejoin="round" d="M10 3v14m-7-7h14" />
                                </svg>
                                Clear all
                            </button>
                        </div>
                    </div>
                </div>

                <div class="flex flex-wrap gap-3" x-show="activeFilters.length" x-cloak>
                    <template x-for="filter in activeFilters" :key="filter">
                        <div
                            class="w-full rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-600 sm:w-auto sm:min-w-[20rem]">
                            <div class="flex items-start justify-between gap-3">
                                <div>
                                    <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500"
                                        x-text="filterOptionLabel(filter)"></div>
                                    <div class="text-sm text-slate-700" x-text="formatFilterValue(filter)"></div>
                                </div>
                                <button type="button"
                                    class="rounded-full p-1 text-slate-400 transition hover:bg-rose-50 hover:text-rose-600"
                                    @click="removeFilter(filter)">
                                    <span class="sr-only">Remove filter</span>
                                    <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                        stroke-width="1.6">
                                        <path stroke-linecap="round" stroke-linejoin="round" d="M6 6l8 8M6 14l8-8" />
                                    </svg>
                                </button>
                            </div>
                            <div class="mt-3 space-y-3">
                                <template x-if="filter === 'direction'">
                                    <select x-model="directionFilter"
                                        class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200">
                                        <template x-for="option in filterOptions.direction" :key="option">
                                            <option :value="option" x-text="directionLabel(option)"></option>
                                        </template>
                                    </select>
                                </template>
                                <template x-if="filter === 'date_range'">
                                    <div class="grid gap-2 sm:grid-cols-2">
                                        <label class="flex flex-col text-[11px] font-semibold text-slate-500">
                                            <span>From</span>
                                            <input type="date" x-model="dateFrom"
                                                class="mt-1 rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200">
                                        </label>
                                        <label class="flex flex-col text-[11px] font-semibold text-slate-500">
                                            <span>To</span>
                                            <input type="date" x-model="dateTo"
                                                class="mt-1 rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200">
                                        </label>
                                    </div>
                                </template>
                                <template x-if="filter === 'only_transcribed'">
                                    <label
                                        class="inline-flex items-center gap-2 rounded border border-slate-200 px-3 py-2 text-sm font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700">
                                        <input type="checkbox"
                                            class="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
                                            x-model="onlyTranscribed">
                                        Transcribed only
                                    </label>
                                </template>
                                <template x-if="filter === 'departments'">
                                    <div class="flex max-h-48 flex-col gap-2 overflow-y-auto pr-1">
                                        <template x-if="!filterOptions.departments.length">
                                            <span
                                                class="rounded-full border border-dashed border-slate-200 px-3 py-1 text-xs text-slate-400">No
                                                departments</span>
                                        </template>
                                        <template x-for="dept in filterOptions.departments" :key="dept.id">
                                            <label
                                                class="inline-flex items-center gap-2 rounded border border-slate-200 px-3 py-1 text-xs font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700">
                                                <input type="checkbox"
                                                    class="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
                                                    :value="String(dept.id)" x-model="selectedDepartments">
                                                <span x-text="dept.name"></span>
                                            </label>
                                        </template>
                                    </div>
                                </template>
                                <template x-if="filter === 'sip_trunks'">
                                    <div class="flex max-h-48 flex-col gap-2 overflow-y-auto pr-1">
                                        <template x-if="!filterOptions.sip_trunks.length">
                                            <span
                                                class="rounded-full border border-dashed border-slate-200 px-3 py-1 text-xs text-slate-400">No
                                                trunks</span>
                                        </template>
                                        <template x-for="trunk in filterOptions.sip_trunks" :key="trunk.id">
                                            <label
                                                class="inline-flex items-center gap-2 rounded border border-slate-200 px-3 py-1 text-xs font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700">
                                                <input type="checkbox"
                                                    class="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
                                                    :value="String(trunk.id)" x-model="selectedSipTrunks">
                                                <span x-text="trunk.display_name || trunk.name"></span>
                                            </label>
                                        </template>
                                    </div>
                                </template>
                                <template x-if="filter === 'tags'">
                                    <div class="flex flex-wrap gap-2">
                                        <template x-if="!filterOptions.tags.length">
                                            <span
                                                class="rounded-full border border-dashed border-slate-200 px-3 py-1 text-xs text-slate-400">No
                                                tags</span>
                                        </template>
                                        <template x-for="tag in filterOptions.tags" :key="tag">
                                            <label
                                                class="inline-flex items-center gap-2 rounded border border-slate-200 px-3 py-1 text-xs font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700">
                                                <input type="checkbox"
                                                    class="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
                                                    :value="tag" x-model="selectedTags">
                                                <span x-text="tag"></span>
                                            </label>
                                        </template>
                                    </div>
                                </template>
                                <template x-if="filter === 'billing_status'">
                                    <select x-model="billingStatusFilter"
                                        class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200">
                                        <template x-for="option in filterOptions.billing_status" :key="option">
                                            <option :value="option" x-text="billingStatusLabel(option)"></option>
                                        </template>
                                    </select>
                                </template>
                            </div>
                        </div>
                    </template>
                </div>
            </div>

            <template x-if="exportedAt">
                <div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-xs text-emerald-700">
                    CSV exported <span class="font-semibold" x-text="exportedAt"></span>
                </div>
            </template>

            <template x-if="error">
                <div class="mt-4 rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-xs text-rose-700"
                    x-text="error"></div>
            </template>

            <div class="mt-4 flex items-center gap-2 text-xs text-slate-500" x-show="loading">
                <svg class="h-4 w-4 animate-spin text-slate-400" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
                        fill="none"></circle>
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
                </svg>
                <span>Loading records…</span>
            </div>

            <template x-if="!filteredRecords.length">
                <div
                    class="mt-8 flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-slate-200 py-16 text-center text-sm text-slate-400">
                    <svg class="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M8 6h13M3 12h18M8 18h13" />
                    </svg>
                    <div>No records match the current filters.</div>
                </div>
            </template>

            <div class="mt-6 overflow-x-auto" x-show="filteredRecords.length">
                <table class="min-w-full divide-y divide-slate-200 text-sm">
                    <thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
                        <tr>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Call</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Started</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Caller</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Callee</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Duration</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Status</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Tags</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Transcript</th>
                            <th scope="col" class="whitespace-nowrap px-4 py-2">Billing</th>
                            <th scope="col"
                                class="sticky right-0 z-10 whitespace-nowrap border-l border-slate-200 bg-gradient-to-l from-white via-white to-slate-50 px-4 py-2 text-right shadow-[-8px_0_12px_-6px_rgba(15,23,42,0.12)]">
                                Actions
                            </th>
                        </tr>
                    </thead>
                    <tbody class="divide-y divide-slate-100">
                        <template x-for="record in paginatedRecords" :key="record.id">
                            <tr class="group align-middle transition hover:bg-slate-50">
                                <td class="whitespace-nowrap px-4 py-2">
                                    <div class="flex gap-x-2 items-center">
                                        <a :href="recordDetailUrl(record)"
                                            class="block font-semibold text-slate-900 transition hover:text-sky-700 hover:underline"
                                            x-text="record.display_id || record.id"></a>
                                        <div class="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
                                            <div class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-semibold"
                                                :class="directionClasses(record.direction)">
                                                <svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="none"
                                                    stroke="currentColor" stroke-width="1.6">
                                                    <path stroke-linecap="round" stroke-linejoin="round"
                                                        d="M5 10h10M10 5l5 5-5 5" />
                                                </svg>
                                                <span x-text="directionLabel(record.direction)"></span>
                                            </div>
                                            <template x-if="record.sip_gateway">
                                                <div class="inline-flex items-center gap-1">
                                                    <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none"
                                                        stroke="currentColor" stroke-width="1.6">
                                                        <path stroke-linecap="round" stroke-linejoin="round"
                                                            d="M4 4h12v12H4z" />
                                                        <path stroke-linecap="round" stroke-linejoin="round"
                                                            d="M8 4v12M12 8h4" />
                                                    </svg>
                                                    <span x-text="record.sip_gateway || '—'"></span>
                                                </div>
                                            </template>
                                        </div>
                                    </div>
                                    <div class="mt-0.5 flex items-center gap-2 text-[11px] text-slate-500">
                                        <span class="max-w-[180px] truncate font-mono" :title="record.call_id"
                                            x-text="record.call_id || '—'"></span>
                                        <template x-if="record.call_id">
                                            <div class="flex items-center gap-1">
                                                <button type="button"
                                                    class="inline-flex h-5 w-5 items-center justify-center rounded border border-slate-200 text-slate-400 transition hover:border-slate-300 hover:text-slate-600"
                                                    title="Copy call ID" aria-label="Copy call ID"
                                                    @click.stop="copyCallId(record.call_id)">
                                                    <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none"
                                                        stroke="currentColor" stroke-width="1.6">
                                                        <path stroke-linecap="round" stroke-linejoin="round"
                                                            d="M6 6h8v8H6z" />
                                                        <path stroke-linecap="round" stroke-linejoin="round"
                                                            d="M4 4h8" />
                                                    </svg>
                                                </button>
                                                <span class="text-[10px] font-semibold text-emerald-600"
                                                    x-show="copiedCallId === record.call_id"
                                                    x-transition.opacity>Copied</span>
                                            </div>
                                        </template>
                                    </div>

                                </td>
                                <td class="px-4 py-2">
                                    <div class="whitespace-nowrap text-[13px] leading-tight text-slate-600"
                                        x-text="formatDateTime(record.started_at)">
                                    </div>
                                </td>
                                <td class="px-4 py-2">
                                    <div class="text-sm font-semibold text-slate-800"
                                        x-text="record.cnam || record.from || '—'"></div>
                                    <div class="text-xs text-slate-500">
                                        <span class="font-mono" x-text="record.from || '—'"></span>
                                    </div>
                                </td>
                                <td class="px-4 py-2">
                                    <div class="text-sm font-semibold text-slate-800"
                                        x-text="record.agent || record.to || '—'"></div>
                                    <div class="text-xs text-slate-500 flex flex-wrap gap-1">
                                        <span class="font-mono" x-text="record.to || '—'"></span>
                                        <span class="text-[11px] text-slate-400" x-show="record.queue"
                                            x-text="record.queue"></span>
                                    </div>
                                </td>
                                <td class="whitespace-nowrap px-4 py-2">
                                    <div class="text-sm font-semibold text-slate-800"
                                        x-text="formatDuration(record.duration_secs)"></div>
                                    <div class="text-xs font-semibold" :class="qualityTone(record.quality?.mos)"
                                        x-text="record.quality && record.quality.mos !== undefined && record.quality.mos !== null ? formatNumber(record.quality.mos) : '—'">
                                    </div>
                                </td>
                                <td class="px-4 py-2">
                                    <span
                                        class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold"
                                        :class="statusClasses(record.status)">
                                        <span class="h-1.5 w-1.5 rounded-full" :class="statusDot(record.status)"></span>
                                        <span x-text="statusLabel(record.status)"></span>
                                    </span>
                                </td>
                                <td class="px-4 py-2">
                                    <div class="flex flex-wrap gap-1 text-[11px]">
                                        <template x-for="tag in (record.tags || [])" :key="record.id + tag">
                                            <span
                                                class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-slate-600">
                                                <span x-text="tag"></span>
                                            </span>
                                        </template>
                                        <template x-if="!(record.tags || []).length">
                                        </template>
                                    </div>
                                </td>
                                <td class="px-4 py-2">
                                    <template
                                        x-if="transcriptStatusIcon(record.transcript_status, record.has_transcript) !== 'pending'">
                                        <div class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold"
                                            :class="transcriptStatusClasses(record.transcript_status, record.has_transcript)"
                                            :title="transcriptStatusLabel(record.transcript_status, record.has_transcript)">
                                            <svg x-cloak
                                                x-show="transcriptStatusIcon(record.transcript_status, record.has_transcript) === 'ready'"
                                                class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                                stroke-width="1.6">
                                                <path stroke-linecap="round" stroke-linejoin="round"
                                                    d="M5 10l3 3 7-7" />
                                            </svg>
                                            <svg x-cloak
                                                x-show="transcriptStatusIcon(record.transcript_status, record.has_transcript) === 'processing'"
                                                class="h-3 w-3 animate-spin" viewBox="0 0 20 20" fill="none"
                                                stroke="currentColor" stroke-width="1.5">
                                                <circle class="opacity-25" cx="10" cy="10" r="7" stroke="currentColor"
                                                    stroke-width="1.5"></circle>
                                                <path class="opacity-75" fill="currentColor"
                                                    d="M10 3a7 7 0 0 1 7 7h-1.8a5.2 5.2 0 0 0-5.2-5.2V3Z" />
                                            </svg>
                                            <svg x-cloak
                                                x-show="transcriptStatusIcon(record.transcript_status, record.has_transcript) === 'failed'"
                                                class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                                stroke-width="1.6">
                                                <path stroke-linecap="round" stroke-linejoin="round"
                                                    d="M6 6l8 8M6 14l8-8" />
                                            </svg>
                                            <span
                                                x-text="transcriptStatusLabel(record.transcript_status, record.has_transcript)"></span>
                                        </div>
                                    </template>
                                </td>
                                <td class="whitespace-nowrap px-4 py-2">
                                    <div class="flex flex-col gap-1 text-xs">
                                        <span class="text-sm font-semibold text-slate-900"
                                            x-text="formatBillingAmount(record.billing)"></span>
                                        <span class="text-[11px] text-slate-400"
                                            x-text="formatBillingMinutes(record.billing)"></span>
                                    </div>
                                </td>
                                <td
                                    class="sticky right-0 z-[1] whitespace-nowrap border-l border-slate-200 bg-gradient-to-l from-white via-white to-slate-50 px-4 py-2 text-right text-xs font-semibold shadow-[-8px_0_12px_-6px_rgba(15,23,42,0.08)] group-hover:from-slate-50 group-hover:via-slate-50 group-hover:to-slate-100">
                                    <div class="flex items-center justify-end gap-2">
                                        <a :href="recordDetailUrl(record)" title="View detail"
                                            class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 text-slate-500 transition hover:border-sky-300 hover:text-sky-700">
                                            <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                                stroke-width="1.6">
                                                <path stroke-linecap="round" stroke-linejoin="round" d="M7 5l6 5-6 5" />
                                            </svg>
                                        </a>
                                        <template x-if="record.recording && record.recording.url">
                                            <a :href="record.recording.url" target="_blank" rel="noopener"
                                                title="Download audio"
                                                class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 text-slate-500 transition hover:border-sky-300 hover:text-sky-700">
                                                <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none"
                                                    stroke="currentColor" stroke-width="1.6">
                                                    <path stroke-linecap="round" stroke-linejoin="round"
                                                        d="M4 12l6 6 6-6" />
                                                    <path stroke-linecap="round" stroke-linejoin="round" d="M10 18V4" />
                                                </svg>
                                            </a>
                                        </template>
                                        <button type="button"
                                            class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-rose-200 text-rose-500 transition hover:border-rose-300 hover:text-rose-600 disabled:cursor-not-allowed disabled:opacity-60"
                                            :disabled="deletingId === record.id" @click.stop="deleteRecord(record)"
                                            title="Delete record">
                                            <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                                stroke-width="1.6">
                                                <path stroke-linecap="round" stroke-linejoin="round"
                                                    d="M6 6h8m-7 3v6m6-6v6M4 6h12" />
                                            </svg>
                                        </button>
                                    </div>
                                </td>
                            </tr>
                        </template>
                    </tbody>
                </table>
                <div
                    class="flex flex-col gap-3 border-t border-slate-100 px-4 py-4 text-xs text-slate-500 sm:flex-row sm:items-center sm:justify-between">
                    <div>
                        <span x-text="filteredRecords.length ? pageRangeStart : 0"></span>
                        –
                        <span x-text="pageRangeEnd"></span>
                        of
                        <span x-text="filteredRecords.length"></span>
                        records
                    </div>
                    <div class="flex flex-wrap items-center gap-3">
                        <label class="flex items-center gap-2">
                            <span class="text-[11px] uppercase tracking-wide text-slate-400">Per page</span>
                            <select x-model.number="pageSize"
                                class="rounded-lg border border-slate-200 px-2 py-1 text-xs text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200">
                                <template x-for="option in pageSizeOptions" :key="option">
                                    <option :value="option" x-text="option"></option>
                                </template>
                            </select>
                        </label>
                        <div
                            class="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white p-1 text-xs font-semibold text-slate-600">
                            <button type="button" class="rounded-full px-2 py-1 transition disabled:opacity-40"
                                :disabled="currentPage === 1" @click="prevPage">
                                Prev
                            </button>
                            <span class="px-2" x-text="`Page ${currentPage} / ${totalPages}`"></span>
                            <button type="button" class="rounded-full px-2 py-1 transition disabled:opacity-40"
                                :disabled="currentPage === totalPages" @click="nextPage">
                                Next
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        </section>
    </div>
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('callRecordsConsole', (payload) => ({
            basePath: '/console',
            listEndpoint: '/console/call-records',
            records: [],
            summary: {
                total: 0,
                answered: 0,
                missed: 0,
                failed: 0,
                transcribed: 0,
                avg_duration: 0,
                total_minutes: 0,
                unique_dids: 0,
                billing_billable_minutes: 0,
                billing_rated_calls: 0,
                billing_charged_calls: 0,
                billing_included_calls: 0,
                billing_zero_duration_calls: 0,
                billing_unrated_calls: 0,
                billing_revenue: [],
            },
            filterOptions: {
                status: ['any'],
                direction: ['any'],
                departments: [],
                sip_trunks: [],
                tags: [],
                billing_status: ['any'],
            },
            search: '',
            statusFilter: 'any',
            directionFilter: 'any',
            dateFrom: '',
            dateTo: '',
            onlyTranscribed: false,
            selectedDepartments: [],
            selectedSipTrunks: [],
            selectedTags: [],
            billingStatusFilter: 'any',
            exportedAt: null,
            loading: false,
            error: null,
            pageSizeOptions: [10, 25, 50],
            pageSize: 10,
            currentPage: 1,
            totalPages: 1,
            totalItems: 0,
            debounceTimer: null,
            copiedCallId: null,
            copyTimer: null,
            deletingId: null,
            detailBase: '',
            _sync: {
                filters: false,
                pageSize: false,
            },
            _activeRequest: null,
            sortOrder: 'started_at_desc',
            sortOptions: [
                { value: 'started_at_desc', label: 'Started · Newest first' },
                { value: 'started_at_asc', label: 'Started · Oldest first' },
                { value: 'duration_desc', label: 'Duration · Longest first' },
                { value: 'duration_asc', label: 'Duration · Shortest first' },
            ],
            filterSelectOptions: [
                { key: 'direction', label: 'Direction' },
                { key: 'date_range', label: 'Date range' },
                { key: 'only_transcribed', label: 'Transcribed only' },
                { key: 'departments', label: 'Departments' },
                { key: 'sip_trunks', label: 'SIP trunks' },
                { key: 'tags', label: 'Tags' },
                { key: 'billing_status', label: 'Billing status' },
            ],
            activeFilters: [],
            persistentFilters: [],
            init() {
                const data = typeof payload === 'string' ? JSON.parse(payload || '{}') : (payload || {});

                if (typeof data.basePath === 'string' && data.basePath) {
                    this.basePath = data.basePath;
                }

                if (typeof data.list_url === 'string' && data.list_url) {
                    this.listEndpoint = data.list_url;
                } else {
                    const normalizedBase = this.basePath === '/'
                        ? ''
                        : (this.basePath.endsWith('/')
                            ? this.basePath.slice(0, -1)
                            : this.basePath);
                    this.listEndpoint = `${normalizedBase}/call-records` || '/call-records';
                }
                if (!this.listEndpoint.startsWith('/')) {
                    this.listEndpoint = `/${this.listEndpoint}`;
                }

                this.detailBase = this.normalizeBasePath(this.basePath);
                this.sortOrder = this.defaultSort();

                const options = (data.filter_options && typeof data.filter_options === 'object')
                    ? data.filter_options
                    : {};
                this.filterOptions = Object.assign({
                    status: ['any'],
                    direction: ['any'],
                    departments: [],
                    sip_trunks: [],
                    tags: [],
                    billing_status: ['any'],
                }, options || {});
                this.filterOptions.status = Array.isArray(this.filterOptions.status)
                    ? this.filterOptions.status
                    : ['any'];
                this.filterOptions.direction = Array.isArray(this.filterOptions.direction)
                    ? this.filterOptions.direction
                    : ['any'];
                this.filterOptions.departments = Array.isArray(this.filterOptions.departments)
                    ? this.filterOptions.departments
                    : [];
                this.filterOptions.sip_trunks = Array.isArray(this.filterOptions.sip_trunks)
                    ? this.filterOptions.sip_trunks
                    : [];
                this.filterOptions.tags = Array.isArray(this.filterOptions.tags)
                    ? this.filterOptions.tags
                    : [];
                this.filterOptions.billing_status = Array.isArray(this.filterOptions.billing_status)
                    ? this.filterOptions.billing_status
                    : ['any'];

                if (Array.isArray(data.page_size_options) && data.page_size_options.length) {
                    const normalized = data.page_size_options
                        .map((value) => Number(value))
                        .filter((value) => Number.isFinite(value) && value > 0);
                    if (normalized.length) {
                        this.pageSizeOptions = normalized;
                    }
                }

                const initialPerPage = Number(data.initial_per_page);
                if (Number.isFinite(initialPerPage) && initialPerPage > 0) {
                    this.pageSize = initialPerPage;
                } else if (this.pageSizeOptions.length) {
                    this.pageSize = this.pageSizeOptions[0];
                }

                if (!this.pageSizeOptions.includes(this.pageSize)) {
                    this.pageSizeOptions = [...this.pageSizeOptions, this.pageSize]
                        .filter((value, index, array) => array.indexOf(value) === index)
                        .sort((a, b) => a - b);
                }

                if (Array.isArray(data.records) && data.records.length) {
                    this.records = data.records;
                }

                this.summary = this.normalizeSummary(data.summary);

                const totalItemsRaw = Number(data.total_items);
                if (Number.isFinite(totalItemsRaw) && totalItemsRaw >= 0) {
                    this.totalItems = totalItemsRaw;
                }

                const totalPagesRaw = Number(data.total_pages);
                if (Number.isFinite(totalPagesRaw) && totalPagesRaw >= 1) {
                    this.totalPages = totalPagesRaw;
                }

                const currentPageRaw = Number(data.page);
                if (Number.isFinite(currentPageRaw) && currentPageRaw >= 1) {
                    this.currentPage = currentPageRaw;
                }

                const scheduleWatcher = () => {
                    if (this._sync.filters) return;
                    this.scheduleFetch();
                };
                const immediateWatcher = () => {
                    if (this._sync.filters) return;
                    this.applyFilters();
                };

                this.$watch('search', scheduleWatcher);
                this.$watch('statusFilter', immediateWatcher);
                this.$watch('directionFilter', immediateWatcher);
                this.$watch('billingStatusFilter', immediateWatcher);
                this.$watch('dateFrom', immediateWatcher);
                this.$watch('dateTo', immediateWatcher);
                this.$watch('onlyTranscribed', immediateWatcher);
                this.$watch('selectedDepartments', immediateWatcher);
                this.$watch('selectedSipTrunks', immediateWatcher);
                this.$watch('selectedTags', immediateWatcher);
                this.$watch('pageSize', (value, oldValue) => {
                    if (this._sync.pageSize) {
                        return;
                    }
                    if (!Number.isFinite(value) || value <= 0 || value === oldValue) {
                        return;
                    }
                    this.applyPageSize(value);
                });

                this.syncActiveFilters();
                if (!Array.isArray(data.records) || !data.records.length) {
                    this.fetchRecords({ page: this.currentPage });
                }
            },
            normalizeBasePath(value) {
                if (!value || value === '/') {
                    return '';
                }
                return value.endsWith('/') ? value.slice(0, -1) : value;
            },
            normalizeSummary(summary) {
                const defaults = {
                    total: 0,
                    answered: 0,
                    missed: 0,
                    failed: 0,
                    transcribed: 0,
                    avg_duration: 0,
                    total_minutes: 0,
                    unique_dids: 0,
                    billing_billable_minutes: 0,
                    billing_rated_calls: 0,
                    billing_charged_calls: 0,
                    billing_included_calls: 0,
                    billing_zero_duration_calls: 0,
                    billing_unrated_calls: 0,
                    billing_revenue: [],
                };
                if (!summary || typeof summary !== 'object') {
                    return defaults;
                }
                return Object.assign({}, defaults, summary);
            },
            scheduleFetch() {
                if (this.debounceTimer) {
                    clearTimeout(this.debounceTimer);
                }
                this.debounceTimer = setTimeout(() => {
                    this.debounceTimer = null;
                    if (this._sync.filters) {
                        return;
                    }
                    this.currentPage = 1;
                    this.syncActiveFilters();
                    this.fetchRecords({ page: 1 });
                }, 300);
            },
            applyFilters() {
                this.currentPage = 1;
                this.syncActiveFilters();
                this.fetchRecords({ page: 1 });
            },
            applySorting() {
                if (this._sync.filters) {
                    return;
                }
                this.currentPage = 1;
                this.syncActiveFilters();
                this.fetchRecords({ page: 1 });
            },
            applyPageSize(size) {
                const perPage = Number(size);
                if (!Number.isFinite(perPage) || perPage <= 0) {
                    return;
                }
                this.currentPage = 1;
                this.syncActiveFilters();
                this.fetchRecords({ page: 1, per_page: perPage });
            },
            buildRequestPayload(overrides = {}) {
                const filters = {};
                const query = (this.search || '').trim();
                if (query) {
                    filters.q = query;
                }

                const status = (this.statusFilter || '').toLowerCase();
                if (status && status !== 'any') {
                    filters.status = status;
                }

                const direction = (this.directionFilter || '').toLowerCase();
                if (direction && direction !== 'any') {
                    filters.direction = direction;
                }

                const billingStatus = (this.billingStatusFilter || '').toLowerCase();
                if (billingStatus && billingStatus !== 'any') {
                    filters.billing_status = billingStatus;
                }

                if (this.dateFrom) {
                    filters.date_from = this.dateFrom;
                }

                if (this.dateTo) {
                    filters.date_to = this.dateTo;
                }

                if (this.onlyTranscribed) {
                    filters.only_transcribed = true;
                }

                const deptIds = (Array.isArray(this.selectedDepartments) ? this.selectedDepartments : [])
                    .map((value) => parseInt(value, 10))
                    .filter((value) => Number.isInteger(value));
                if (deptIds.length) {
                    filters.department_ids = deptIds;
                }

                const sipIds = (Array.isArray(this.selectedSipTrunks) ? this.selectedSipTrunks : [])
                    .map((value) => parseInt(value, 10))
                    .filter((value) => Number.isInteger(value));
                if (sipIds.length) {
                    filters.sip_trunk_ids = sipIds;
                }

                const tags = (Array.isArray(this.selectedTags) ? this.selectedTags : [])
                    .map((value) => (value || '').toString().trim())
                    .filter((value) => value.length)
                    .filter((value, index, array) => array.indexOf(value) === index);
                if (tags.length) {
                    filters.tags = tags;
                }

                const page = Number.isFinite(overrides.page) && overrides.page ? overrides.page : this.currentPage;
                const perPage = Number.isFinite(overrides.per_page) && overrides.per_page > 0
                    ? overrides.per_page
                    : this.pageSize;

                const payload = {
                    page: Math.max(1, page),
                    per_page: Math.max(1, perPage),
                };

                if (Object.keys(filters).length) {
                    payload.filters = filters;
                }

                if (this.sortOrder) {
                    payload.sort = this.sortOrder;
                }

                return payload;
            },
            async fetchRecords(options = {}) {
                const token = Symbol('call-records-request');
                this._activeRequest = token;
                this.loading = true;
                this.error = null;
                try {
                    const payload = this.buildRequestPayload(options);
                    const response = await fetch(this.listEndpoint, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify(payload),
                    });
                    const data = await response.json();
                    if (!response.ok) {
                        throw new Error(data?.message || 'Failed to load call records');
                    }

                    if (this._activeRequest !== token) {
                        return;
                    }

                    const items = Array.isArray(data?.items) ? data.items : [];
                    this.records = items;

                    const perPageRaw = Number(data?.per_page);
                    const totalItemsRaw = Number(data?.total_items);
                    const totalPagesRaw = Number(data?.total_pages);
                    const currentPageRaw = Number(data?.page);

                    const perPage = Number.isFinite(perPageRaw) && perPageRaw > 0
                        ? perPageRaw
                        : payload.per_page;

                    if (this.pageSize !== perPage) {
                        this._sync.pageSize = true;
                        this.pageSize = perPage;
                        this.$nextTick(() => {
                            this._sync.pageSize = false;
                        });
                    }

                    const totalItems = Number.isFinite(totalItemsRaw) && totalItemsRaw >= 0
                        ? totalItemsRaw
                        : items.length;
                    const inferredTotalPages = Math.max(Math.ceil(totalItems / (perPage || 1)), 1);
                    const totalPages = Number.isFinite(totalPagesRaw) && totalPagesRaw >= 1
                        ? Math.max(totalPagesRaw, 1)
                        : inferredTotalPages;
                    const currentPage = Number.isFinite(currentPageRaw) && currentPageRaw >= 1
                        ? Math.min(currentPageRaw, totalPages)
                        : Math.min(payload.page, totalPages);

                    this.currentPage = currentPage;
                    this.totalItems = totalItems;
                    this.totalPages = totalPages;
                    this.summary = this.normalizeSummary(data?.summary);
                } catch (err) {
                    console.error(err);
                    if (this._activeRequest !== token) {
                        return;
                    }
                    this.error = err?.message || 'Unable to load call records';
                    this.records = [];
                    this.summary = this.normalizeSummary(null);
                    this.totalItems = 0;
                    this.totalPages = 1;
                    this.currentPage = 1;
                } finally {
                    if (this._activeRequest === token) {
                        this.loading = false;
                        this.syncActiveFilters();
                    }
                }
            },
            setPage(page) {
                const target = Math.min(
                    Math.max(Number(page) || 1, 1),
                    Math.max(this.totalPages || 1, 1),
                );
                if (target === this.currentPage) {
                    return;
                }
                this.currentPage = target;
                this.fetchRecords({ page: target });
            },
            prevPage() {
                if (this.currentPage > 1) {
                    this.setPage(this.currentPage - 1);
                }
            },
            nextPage() {
                if (this.currentPage < this.totalPages) {
                    this.setPage(this.currentPage + 1);
                }
            },
            get filteredRecords() {
                return Array.isArray(this.records) ? this.records : [];
            },
            get paginatedRecords() {
                return this.filteredRecords;
            },
            get pageRangeStart() {
                if (!this.filteredRecords.length) {
                    return 0;
                }
                return ((this.currentPage - 1) * this.pageSize) + 1;
            },
            get pageRangeEnd() {
                if (!this.filteredRecords.length) {
                    return 0;
                }
                return Math.min(this.pageRangeStart + this.filteredRecords.length - 1, this.totalItems);
            },
            clearFilters() {
                if (this.debounceTimer) {
                    clearTimeout(this.debounceTimer);
                    this.debounceTimer = null;
                }
                this._sync.filters = true;
                this.search = '';
                this.statusFilter = 'any';
                this.directionFilter = 'any';
                this.billingStatusFilter = 'any';
                this.dateFrom = '';
                this.dateTo = '';
                this.onlyTranscribed = false;
                this.selectedDepartments = [];
                this.selectedSipTrunks = [];
                this.selectedTags = [];
                this.sortOrder = this.defaultSort();
                this.activeFilters = [];
                this.persistentFilters = [];
                this.$nextTick(() => {
                    this._sync.filters = false;
                    this.applyFilters();
                });
            },
            async copyCallId(callId) {
                if (!callId) {
                    return;
                }
                const text = String(callId);
                let ok = await this.tryClipboardWrite(text);
                if (!ok) {
                    ok = this.copyCallIdFallback(text);
                }
                if (ok) {
                    this.showCopyConfirmation(text);
                }
            },
            async tryClipboardWrite(text) {
                if (typeof navigator === 'undefined' || !navigator.clipboard || !navigator.clipboard.writeText) {
                    return false;
                }
                try {
                    await navigator.clipboard.writeText(text);
                    return true;
                } catch (err) {
                    console.error('Clipboard API failed', err);
                    return false;
                }
            },
            showCopyConfirmation(value) {
                this.copiedCallId = value;
                if (this.copyTimer) {
                    clearTimeout(this.copyTimer);
                }
                this.copyTimer = setTimeout(() => {
                    this.copiedCallId = null;
                    this.copyTimer = null;
                }, 1500);
            },
            // Provide a best-effort fallback for legacy clipboard implementations.
            copyCallIdFallback(text) {
                try {
                    const temp = document.createElement('textarea');
                    temp.value = text;
                    temp.setAttribute('readonly', '');
                    temp.style.position = 'fixed';
                    temp.style.opacity = '0';
                    document.body.appendChild(temp);
                    temp.select();
                    temp.setSelectionRange(0, temp.value.length);
                    const success = document.execCommand('copy');
                    document.body.removeChild(temp);
                    return success;
                } catch (err) {
                    console.error('Copy fallback failed', err);
                    return false;
                }
            },
            recordDetailUrl(record) {
                if (record && record.detail_url) {
                    return record.detail_url;
                }
                const id = record && record.id;
                if (!id) {
                    return '#';
                }
                const base = this.detailBase ? this.detailBase : '';
                const path = `${base}/call-records/${encodeURIComponent(id)}`;
                return path.startsWith('/') ? path : `/${path}`;
            },
            recordDeleteUrl(record) {
                if (record && record.detail_url) {
                    return record.detail_url;
                }
                const id = record && record.id;
                if (!id) {
                    return '#';
                }
                const base = this.detailBase ? this.detailBase : '';
                const path = `${base}/call-records/${encodeURIComponent(id)}`;
                return path.startsWith('/') ? path : `/${path}`;
            },
            async deleteRecord(record) {
                if (!record || !record.id) {
                    return;
                }
                if (typeof window === 'undefined' || !window?.dispatchEvent) {
                    this.executeDelete(record);
                    return;
                }
                window.dispatchEvent(new CustomEvent('console:confirm', {
                    detail: {
                        title: 'Delete call record',
                        message: 'This will permanently remove the call record and associated metadata.',
                        confirmLabel: 'Delete',
                        cancelLabel: 'Cancel',
                        destructive: true,
                        onConfirm: () => this.executeDelete(record),
                    },
                }));
            },
            async executeDelete(record) {
                if (!record || !record.id) {
                    return;
                }
                this.deletingId = record.id;
                this.error = null;
                const isLastItemOnPage = this.records.length <= 1;
                const targetPage = isLastItemOnPage && this.currentPage > 1
                    ? this.currentPage - 1
                    : this.currentPage;
                try {
                    const response = await fetch(this.recordDeleteUrl(record), {
                        method: 'DELETE',
                        headers: {
                            'Accept': 'application/json',
                        },
                    });
                    if (!response.ok) {
                        let message = 'Failed to delete record';
                        try {
                            const data = await response.json();
                            if (data && data.message) {
                                message = data.message;
                            }
                        } catch (_) {
                            // Response had no body to parse; continue with default message
                        }
                        throw new Error(message);
                    }
                    this.records = this.records.filter((item) => item.id !== record.id);
                    this.currentPage = targetPage;
                    await this.fetchRecords({ page: targetPage });
                } catch (err) {
                    console.error(err);
                    this.error = err && err.message ? err.message : 'Failed to delete record';
                } finally {
                    this.deletingId = null;
                }
            },
            async exportCsv() {
                try {
                    this.exportedAt = null;
                    const desired = Math.max(this.totalItems || 0, this.pageSize || 1);
                    const payload = this.buildRequestPayload({
                        page: 1,
                        per_page: Math.min(Math.max(desired, 1000), 10000),
                    });
                    const response = await fetch(this.listEndpoint, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify(payload),
                    });
                    const data = await response.json();
                    if (!response.ok) {
                        throw new Error(data?.message || 'Failed to export CSV');
                    }
                    const rows = Array.isArray(data?.items) ? data.items : [];
                    if (!rows.length) {
                        this.exportedAt = 'No data to export';
                        return;
                    }
                    const csv = this.buildCsv(rows);
                    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
                    const url = URL.createObjectURL(blob);
                    const link = document.createElement('a');
                    link.href = url;
                    link.download = `call-records-${new Date().toISOString().slice(0, 10)}.csv`;
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    URL.revokeObjectURL(url);
                    this.exportedAt = new Date().toLocaleString();
                } catch (err) {
                    console.error(err);
                    this.exportedAt = err?.message || 'Export failed';
                }
            },
            buildCsv(records) {
                const header = [
                    'Call ID',
                    'Display ID',
                    'Started At',
                    'Direction',
                    'Status',
                    'From',
                    'To',
                    'Duration (s)',
                    'Agent',
                    'Queue',
                    'Tags',
                    'Transcript',
                    'Billing status',
                    'Billing total',
                ];
                return [header]
                    .concat(
                        records.map((record) => [
                            record?.id || '',
                            record?.display_id || '',
                            this.formatDateTime(record?.started_at),
                            this.directionLabel(record?.direction),
                            this.statusLabel(record?.status),
                            record?.from || '',
                            record?.to || '',
                            record?.duration_secs || 0,
                            record?.agent || '',
                            record?.queue || '',
                            (Array.isArray(record?.tags) ? record.tags : []).join('|'),
                            record?.has_transcript ? 'Yes' : 'No',
                            this.billingStatusLabel(record?.billing?.status),
                            this.formatBillingAmount(record?.billing),
                        ])
                    )
                    .map((line) =>
                        line
                            .map((cell) => {
                                const value = cell === null || cell === undefined ? '' : String(cell);
                                const escaped = value.replace(/"/g, '""');
                                return `"${escaped}"`;
                            })
                            .join(',')
                    )
                    .join('\n');
            },
            statusTabs() {
                const options = Array.isArray(this.filterOptions.status) ? this.filterOptions.status : [];
                return options
                    .filter((value) => value !== 'any')
                    .map((value) => ({
                        value,
                        label: this.statusLabel(value),
                    }));
            },
            directionTabs() {
                const options = Array.isArray(this.filterOptions.direction) ? this.filterOptions.direction : [];
                return options
                    .filter((value) => value !== 'any')
                    .map((value) => ({
                        value,
                        label: this.directionLabel(value),
                    }));
            },
            selectStatusTab(value) {
                this.statusFilter = this.statusFilter === value ? 'any' : value;
            },
            selectDirectionTab(value) {
                this.directionFilter = this.directionFilter === value ? 'any' : value;
            },
            filterOptionLabel(key) {
                switch (key) {
                    case 'status':
                        return 'Status';
                    case 'direction':
                        return 'Direction';
                    case 'date_range':
                        return 'Date range';
                    case 'only_transcribed':
                        return 'Transcribed only';
                    case 'departments':
                        return 'Departments';
                    case 'sip_trunks':
                        return 'SIP trunks';
                    case 'tags':
                        return 'Tags';
                    case 'billing_status':
                        return 'Billing status';
                    default:
                        return key;
                }
            },
            formatFilterValue(key) {
                switch (key) {
                    case 'status':
                        return this.statusLabel(this.statusFilter);
                    case 'direction':
                        return this.directionFilter && this.directionFilter !== 'any'
                            ? this.directionLabel(this.directionFilter)
                            : 'Any direction';
                    case 'date_range':
                        if (this.dateFrom && this.dateTo) {
                            return `${this.dateFrom} → ${this.dateTo}`;
                        }
                        if (this.dateFrom) {
                            return `From ${this.dateFrom}`;
                        }
                        if (this.dateTo) {
                            return `Until ${this.dateTo}`;
                        }
                        return 'No range selected';
                    case 'only_transcribed':
                        return this.onlyTranscribed ? 'Enabled' : 'Disabled';
                    case 'departments': {
                        const options = Array.isArray(this.filterOptions.departments)
                            ? this.filterOptions.departments
                            : [];
                        const selected = Array.isArray(this.selectedDepartments)
                            ? this.selectedDepartments
                            : [];
                        if (!selected.length) {
                            return 'Any department';
                        }
                        const names = selected
                            .map((id) => {
                                const numeric = Number(id);
                                const match = options.find((dept) => Number(dept.id) === numeric);
                                if (match) {
                                    return match.name;
                                }
                                return Number.isFinite(numeric) ? `#${numeric}` : String(id);
                            })
                            .filter(Boolean);
                        return names.length ? names.join(', ') : 'Any department';
                    }
                    case 'sip_trunks': {
                        const options = Array.isArray(this.filterOptions.sip_trunks)
                            ? this.filterOptions.sip_trunks
                            : [];
                        const selected = Array.isArray(this.selectedSipTrunks)
                            ? this.selectedSipTrunks
                            : [];
                        if (!selected.length) {
                            return 'Any trunk';
                        }
                        const names = selected
                            .map((id) => {
                                const numeric = Number(id);
                                const match = options.find((trunk) => Number(trunk.id) === numeric);
                                if (!match) {
                                    return Number.isFinite(numeric) ? `#${numeric}` : String(id);
                                }
                                return match.display_name || match.name || `#${numeric}`;
                            })
                            .filter(Boolean);
                        return names.length ? names.join(', ') : 'Any trunk';
                    }
                    case 'tags': {
                        const selected = Array.isArray(this.selectedTags) ? this.selectedTags : [];
                        return selected.length ? selected.join(', ') : 'Any tag';
                    }
                    case 'billing_status':
                        return this.billingStatusFilter && this.billingStatusFilter !== 'any'
                            ? this.billingStatusLabel(this.billingStatusFilter)
                            : 'Any billing status';
                    default:
                        return '';
                }
            },
            addFilter(event) {
                const key = event?.target?.value;
                if (!key) {
                    return;
                }
                if (!this.activeFilters.includes(key)) {
                    this.activeFilters = [...this.activeFilters, key];
                }
                if (this.shouldPersistFilter(key) && !this.persistentFilters.includes(key)) {
                    this.persistentFilters = [...this.persistentFilters, key];
                }
                if (event && event.target) {
                    event.target.value = '';
                }
                this.syncActiveFilters();
            },
            removeFilter(key) {
                if (!key) {
                    return;
                }
                if (this.debounceTimer) {
                    clearTimeout(this.debounceTimer);
                    this.debounceTimer = null;
                }
                this._sync.filters = true;
                switch (key) {
                    case 'status':
                        this.statusFilter = 'any';
                        break;
                    case 'direction':
                        this.directionFilter = 'any';
                        break;
                    case 'date_range':
                        this.dateFrom = '';
                        this.dateTo = '';
                        break;
                    case 'only_transcribed':
                        this.onlyTranscribed = false;
                        break;
                    case 'departments':
                        this.selectedDepartments = [];
                        break;
                    case 'sip_trunks':
                        this.selectedSipTrunks = [];
                        break;
                    case 'tags':
                        this.selectedTags = [];
                        break;
                    case 'billing_status':
                        this.billingStatusFilter = 'any';
                        break;
                    default:
                        break;
                }
                this.activeFilters = this.activeFilters.filter((item) => item !== key);
                if (this.shouldPersistFilter(key)) {
                    this.persistentFilters = this.persistentFilters.filter((item) => item !== key);
                }
                this.$nextTick(() => {
                    this._sync.filters = false;
                    this.syncActiveFilters();
                    this.applyFilters();
                });
            },
            shouldPersistFilter(key) {
                return ['direction', 'date_range', 'only_transcribed', 'departments', 'sip_trunks', 'tags'].includes(key);
            },
            syncActiveFilters() {
                const manualFilters = Array.isArray(this.activeFilters)
                    ? [...this.activeFilters]
                    : [];
                const statusActive = this.statusFilter && this.statusFilter !== 'any';
                const directionActive = this.directionFilter && this.directionFilter !== 'any';
                const billingStatusActive = this.billingStatusFilter && this.billingStatusFilter !== 'any';
                const hasDate = Boolean(this.dateFrom) || Boolean(this.dateTo);
                const departmentsSelected = Array.isArray(this.selectedDepartments) && this.selectedDepartments.length;
                const sipSelected = Array.isArray(this.selectedSipTrunks) && this.selectedSipTrunks.length;
                const tagsSelected = Array.isArray(this.selectedTags) && this.selectedTags.length;
                const next = [];
                const addUnique = (key) => {
                    if (!next.includes(key)) {
                        next.push(key);
                    }
                };

                (this.persistentFilters || []).forEach((key) => addUnique(key));

                if (statusActive) {
                    addUnique('status');
                }
                if (directionActive || this.persistentFilters.includes('direction')) {
                    addUnique('direction');
                }
                if (billingStatusActive || manualFilters.includes('billing_status')) {
                    addUnique('billing_status');
                }
                if (hasDate || this.persistentFilters.includes('date_range')) {
                    addUnique('date_range');
                }
                if (this.onlyTranscribed || this.persistentFilters.includes('only_transcribed')) {
                    addUnique('only_transcribed');
                }
                if (departmentsSelected || this.persistentFilters.includes('departments')) {
                    addUnique('departments');
                }
                if (sipSelected || this.persistentFilters.includes('sip_trunks')) {
                    addUnique('sip_trunks');
                }
                if (tagsSelected || this.persistentFilters.includes('tags')) {
                    addUnique('tags');
                }

                this.activeFilters = next;
            },
            defaultSort() {
                return 'started_at_desc';
            },
            statusLabel(value) {
                switch ((value || '').toLowerCase()) {
                    case 'completed':
                        return 'Completed';
                    case 'missed':
                        return 'Missed';
                    case 'failed':
                        return 'Failed';
                    case 'any':
                        return 'Any status';
                    default:
                        return value || 'Unknown';
                }
            },
            directionLabel(value) {
                switch ((value || '').toLowerCase()) {
                    case 'inbound':
                        return 'Inbound';
                    case 'outbound':
                        return 'Outbound';
                    case 'internal':
                        return 'Internal';
                    case 'any':
                        return 'Any direction';
                    default:
                        return value || 'Unknown';
                }
            },
            billingStatusLabel(value) {
                switch ((value || '').toLowerCase()) {
                    case 'charged':
                        return 'Charged';
                    case 'included':
                        return 'Included';
                    case 'zero-duration':
                        return 'Zero duration';
                    case 'unrated':
                        return 'Unrated';
                    case 'any':
                        return 'Any billing status';
                    default:
                        return value || 'Unknown';
                }
            },
            transcriptStatusLabel(status, hasTranscript = false) {
                const value = (status || '').toLowerCase();
                if (value === 'completed' || hasTranscript) {
                    return 'Ready';
                }
                if (value === 'processing') {
                    return 'Processing';
                }
                if (value === 'failed') {
                    return 'Failed';
                }
                return 'Pending';
            },
            transcriptStatusClasses(status, hasTranscript = false) {
                const value = (status || '').toLowerCase();
                if (value === 'completed' || hasTranscript) {
                    return 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200';
                }
                if (value === 'processing') {
                    return 'bg-amber-50 text-amber-600 ring-1 ring-amber-200';
                }
                if (value === 'failed') {
                    return 'bg-rose-50 text-rose-600 ring-1 ring-rose-200';
                }
                return 'bg-slate-100 text-slate-500 ring-1 ring-slate-200';
            },
            transcriptStatusIcon(status, hasTranscript = false) {
                const value = (status || '').toLowerCase();
                if (value === 'completed' || hasTranscript) {
                    return 'ready';
                }
                if (value === 'processing') {
                    return 'processing';
                }
                if (value === 'failed') {
                    return 'failed';
                }
                return 'pending';
            },
            statusClasses(status) {
                switch ((status || '').toLowerCase()) {
                    case 'completed':
                        return 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200';
                    case 'missed':
                        return 'bg-amber-50 text-amber-600 ring-1 ring-amber-200';
                    case 'failed':
                        return 'bg-rose-50 text-rose-600 ring-1 ring-rose-200';
                    default:
                        return 'bg-slate-100 text-slate-600 ring-1 ring-slate-200';
                }
            },
            billingStatusClasses(status) {
                switch ((status || '').toLowerCase()) {
                    case 'charged':
                        return 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200';
                    case 'included':
                        return 'bg-sky-50 text-sky-600 ring-1 ring-sky-200';
                    case 'zero-duration':
                        return 'bg-slate-100 text-slate-600 ring-1 ring-slate-200';
                    case 'unrated':
                        return 'bg-amber-50 text-amber-600 ring-1 ring-amber-200';
                    default:
                        return 'bg-slate-100 text-slate-600 ring-1 ring-slate-200';
                }
            },
            statusDot(status) {
                switch ((status || '').toLowerCase()) {
                    case 'completed':
                        return 'bg-emerald-500';
                    case 'missed':
                        return 'bg-amber-500';
                    case 'failed':
                        return 'bg-rose-500';
                    default:
                        return 'bg-slate-400';
                }
            },
            directionClasses(direction) {
                switch ((direction || '').toLowerCase()) {
                    case 'inbound':
                        return 'border border-sky-100 bg-sky-50 text-sky-700';
                    case 'outbound':
                        return 'border border-emerald-100 bg-emerald-50 text-emerald-700';
                    case 'internal':
                        return 'border border-slate-200 bg-slate-50 text-slate-700';
                    default:
                        return 'border border-slate-200 bg-slate-50 text-slate-700';
                }
            },
            formatCurrency(amount, currency) {
                if (amount === null || amount === undefined) {
                    return '—';
                }
                const numeric = Number(amount);
                if (!Number.isFinite(numeric)) {
                    return '—';
                }
                const symbol = (currency || 'USD').toString().trim().toUpperCase() || 'USD';
                return `${symbol} ${numeric.toLocaleString(undefined, {
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2,
                })}`;
            },
            formatBillingAmount(billing) {
                if (!billing || typeof billing !== 'object') {
                    return '—';
                }
                const status = (billing.status || '').toLowerCase();
                const amount = billing.amount || {};
                const total = amount.total ?? amount.subtotal ?? null;
                if (total !== null && total !== undefined) {
                    return this.formatCurrency(total, billing.currency || 'USD');
                }
                if (status === 'included') {
                    return 'Included';
                }
                if (status === 'zero-duration') {
                    return '0.00';
                }
                if (status === 'unrated') {
                    return 'Unrated';
                }
                return '—';
            },
            formatBillingMinutes(billing) {
                if (!billing || typeof billing !== 'object') {
                    return '—';
                }
                const source = billing.billable_minutes !== undefined
                    ? Number(billing.billable_minutes)
                    : (typeof billing.billable_secs === 'number'
                        ? Number(billing.billable_secs) / 60.0
                        : NaN);
                if (!Number.isFinite(source) || source <= 0) {
                    return '—';
                }
                return `${source.toLocaleString(undefined, {
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2,
                })} min`;
            },
            billingRevenueEntries() {
                const entries = Array.isArray(this.summary.billing_revenue)
                    ? this.summary.billing_revenue
                    : [];
                return entries
                    .map((entry) => ({
                        currency: (entry?.currency || 'USD').toString().trim().toUpperCase() || 'USD',
                        total: Number(entry?.total ?? 0),
                    }))
                    .filter((entry) => Number.isFinite(entry.total));
            },
            billingSummaryTotal() {
                const entries = this.billingRevenueEntries();
                if (!entries.length) {
                    return '—';
                }
                return entries
                    .map((entry) => this.formatCurrency(entry.total, entry.currency))
                    .join(' · ');
            },
            qualityTone(value) {
                const mos = Number(value || 0);
                if (mos >= 4.3) {
                    return 'text-emerald-600';
                }
                if (mos >= 3.8) {
                    return 'text-amber-600';
                }
                if (mos > 0) {
                    return 'text-rose-600';
                }
                return 'text-slate-500';
            },
            formatNumber(value) {
                if (value === null || value === undefined || Number.isNaN(Number(value))) {
                    return '—';
                }
                return Number(value).toLocaleString(undefined, { maximumFractionDigits: 1 });
            },
            formatAvgDuration(value) {
                if (!value) {
                    return '0s';
                }
                const seconds = Number(value);
                if (Number.isNaN(seconds)) {
                    return '—';
                }
                if (seconds >= 3600) {
                    const hours = Math.floor(seconds / 3600);
                    const minutes = Math.round((seconds % 3600) / 60);
                    return `${hours}h ${minutes}m`;
                }
                if (seconds >= 60) {
                    const minutes = Math.floor(seconds / 60);
                    const secs = Math.round(seconds % 60);
                    return `${minutes}m ${secs}s`;
                }
                return `${Math.round(seconds)}s`;
            },
            formatDuration(value) {
                const seconds = Number(value || 0);
                if (!seconds) {
                    return '00:00';
                }
                const hours = Math.floor(seconds / 3600);
                const minutes = Math.floor((seconds % 3600) / 60);
                const secs = Math.floor(seconds % 60);
                const pad = (num) => String(num).padStart(2, '0');
                if (hours > 0) {
                    return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
                }
                return `${pad(minutes)}:${pad(secs)}`;
            },
            formatDateTime(value) {
                if (!value) {
                    return '—';
                }
                try {
                    const date = new Date(value);
                    if (Number.isNaN(date.getTime())) {
                        return value;
                    }
                    return date.toLocaleString();
                } catch (err) {
                    return value;
                }
            },
        }));
    });
</script>
{% endblock %}